Groovy 和 Jenkins pipeline
公司项目一直使用的 Jenkins 作为 java/Android 的 CI/CD 工具,之前需求比较简单,直接使用 Jenkins 提供的管理后台 UI 操作就能完成。后来有一些新的构建需求,我最初是使用 Python 处理的,近期全部转到了 gradle 脚本 + Jenkins pipeline 的实现。这两者使用的都是 groovy,使用过程中发现,其实 groovy 很多操作比 Python 都要方便。
先介绍一些 groovy 操作便捷和需要注意的地方
File
join file path
1 | def dataPath = Paths.get("/User/xxx","data/log") |
读写文件
1 | new File(".").text |
更多示例可以参考:groovy-io
正则
groovy 提供了很多字符串表示正则的写法:
'\d+'
单引号"\d+"
双引号/\d+/
斜杠$\d+$
美元符号
*我使用斜杠较多,可以应付很多需要转义的情况
groovy 还提供了很多特有的操作符:
~
模式符,后面紧跟正则表达式字符串即会转换为java.util.regex.Pattern
对象, 如:1
2def p = ~/\d+/
// p 就是一个 Pattern 对象了=~
正则搜寻符,相当与执行正则的 find 操作,返回 Matcher 对象,在 groovy 里可以直接对其进行遍历:1
2
3
4def matcher = result =~ /versionCode='(\S+)'\s+versionName='(\S+)'/
// matcher[0] 就是搜索结果,具体group结果在子数组里
def version_code = matcher[0][1]
def version_name = matcher[0][2]==~
正则匹配符,相当与执行 match 操作,返回值 boolean 类型
执行 shell 命令
直接 'ls -al'.execute().text
就可以执行并拿到结果,非常方便,更 Robust 的代码:
1 | def result = new StringBuilder() |
List/Map
- 创建
- list:
def a = []
- map 就不太一样了:
def m = [:]
,def m = [name: "John", age:123]
- list:
find
,findAll
,collect
find
和findAll
起到过滤作用,前者返回一个元素,后者返回集合; 相当于其他语言里的map
转换方法。
变量作用域的问题
1 | // foo.groovy |
这里top-level的俩变量是不一样的,x 没有类型声明,将触发 script binding,变成“全局变量”(script-scope),而 y 是本地变量,除了在top-level直接使用外,无法被其他方法调用。实际上看看 groovy 转换后的代码就一目了然了:
1 | class foo{ |
但是,不写任何声明的变量在 gradle 中是不能使用的,Android gradle 中如果想实现 script-scope variable,只能通过给 赋给 project/ext 的方式实现:
1 | // build.gradle |
其他
- 强制转换符
as
, 这和 Kotlin 的还不一样:如果是不同类型,groovy 这实际上是创建了一个新的对象,而这个转换方法(1
2
3
4
5
6
7Integer x = 123
// Java ,直接抛出 ClassCastException 异常
String s = (String) x
// Kotlin ,同 Java
String s = x as String
// Groovy ,正常转换到 String
String s = x as StringasType()
)是需要实现的,如果是自定义类型那需要我们自己实现。具体参考 Coercion operator
Jenkins Pipeline
Pipeline 是 Jenkins 提供的一种持续交付的模式,我们可以通过编写 pipeline 脚本文件 (Jenkinsfile) 来高效地实现我们的 CD 流程。
Jenkinsfile 脚本文件分为两种:
Declarative Pipeline
集成了很多 Jenkins 新提供的很多 Api 功能,可以声明式的便捷调用。
比如 jenkins 构建完成后你要发一条结果通知:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28pipeline {
agent any
stages {
stage('No-op') {
steps {
sh 'ls'
}
}
}
post {
always {
echo 'One way or another, I have finished'
deleteDir() /* clean up our workspace */
}
success {
echo 'I succeeded!'
}
unstable {
echo 'I am unstable :/'
}
failure {
echo 'I failed :('
}
changed {
echo 'Things were different before...'
}
}
}
post
就是完成后会执行的方法,
Scripted Pipeline
纯“脚本式”的风格,Jenkins 提供的 api 很少。
同样的上面的需求, scripted 实现是这样:
1 | node { |
注意,只能使用 try catch
的方式实现(currentBuild
是由 jenkins 提供的,可以直接使用)。
另外,从上面俩例子也可以说明两者的格式也是不一样的,前者整个构建过程需要这样表述:
1 | pipeline { |
pipeline 代码块是声明式的核心主体,其包含多个 stage,表示当前执行的阶段,名称可以自定义,stage 里又由多个 steps 构成,表示执行的具体步骤。
1 | node { |
node 代码块则是脚本式的核心主体,其直接包含多个 stage(其实有没有 stage 不重要),我们把具体的执行代码直接放到对应的 stage 下即可,这里没有 step 等内容。写 stage 的主要目的是可以在 jenkins 有直观的 ui 展示,比较方便。
pipeline 脚本主要基于 groovy,声明式和脚本式的环境、插件都是共用的(调用方式上会有些不一样),声明式出现的目的是 Jenkins 团队认为 groovy 的学习曲线还是比较陡峭,所以整了个声明式的,方便不了解 groovy 语法的开发者也可以使用 pipeline。Syntax Comparison
Jenkins 部分 Api 说明
dir
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16node {
// 工作目录 /workspace
stage("Build"){
dir('demo'){
// 创建并切换到 /workspace/demo 目录下
// git clone, credentialsId 需预先在 jenkins 中配置
git branch: 'develop',
url: '',
credentialsId: ''
}
// 自动回到工作目录
dir("demolib"){
// 创建并切换到 /workspace/demolib 目录下
}
}
}- Jenkins pipeline 里执行 shell 脚本注意事项:
- 插入 pipeline 中定义的变量,需要用
'''+variable+'''
方式插入:1
2
3
4
5
6
7
8
9
10node {
def f = "rtm"
stage('Build'){
sh '''
./gradlew '''+f+'''Release
'''
}
}